Skip to content

[r3.6] execution/cache: prevent dead-fork StateCache fills across unwind - #23260

Merged
yperbasis merged 1 commit into
release/3.6from
cherry-pick-23005-to-release-3.6
Aug 14, 2026
Merged

[r3.6] execution/cache: prevent dead-fork StateCache fills across unwind#23260
yperbasis merged 1 commit into
release/3.6from
cherry-pick-23005-to-release-3.6

Conversation

@yperbasis

Copy link
Copy Markdown
Member

Cherry-pick of #23005 to release/3.6.

r3.6-specific adaptations

  • Resolved read-ahead conflicts against the release API, which has no in-memory block-access-list cache.
  • Adapted the state-version regression test because release/3.6 reads DISCARD_COMMITMENT at process startup.
  • Kept only execution/cache: prevent dead-fork StateCache fills across unwind #23005-specific tests where main contains tests for unrelated newer behavior.

…3005)

Fixes #22463.

`StateCache` stores latest committed state. Unwind already made resident
dead-fork entries stale, but readers could add those values again from
an old or transient view: a transaction could survive or first bind
during unwind, staged unwind rows still existed in the backing database,
and read-ahead could fill concurrently.

This PR closes those windows by binding fill authority to both the
durable `PlainStateVersion` and the lifetime of the original `ReadView`.
Reads constrained by a staged unwind cannot fill, cache changes are
published only after the database commit, and read-ahead cannot cross
the unwind transition.

Snapshot and immutable-file publication are a separate coherence
boundary. #23028 still requires #23047 or an equivalent publication hook
and is not addressed here. Bounded speculative-unwind fills in the
separate commitment `BranchCache` pre-exist this PR and are tracked in

Suggested order:

1. `execution/cache/view.go` and `state_cache.go`: fill admission and
publication.
2. `db/state/execctx/domain_shared.go`: transaction identity, bounded
reads, and commit/unwind integration.
3. `db/kv/membatchwithdb/memory_mutation.go` and
`db/state/temporal_mem_batch.go`: `PlainStateVersion` ownership and
monotonicity.
4. `execution/exec/blocks_read_ahead.go` and `execution/execmodule`:
read-ahead exclusion and lifecycle.

Focused regression tests sit beside each area.

| Marker | Protects |
| --- | --- |
| `PlainStateVersion` | The durable state visible to a transaction |
| `readViewEpoch` | Whether a `ReadView` predates the latest unwind or
state discontinuity |
| Per-cache entry epoch and unwind floor | Whether a stored value
belongs to the retained fork |

Once the cache has a durable state version, an admission-gated state
fill is accepted only if the view has the published state version and
current epoch, publication is not in progress, its exact domain frontier
is not behind the cache, and the read has no staged-unwind step bound.
Content-addressed code-size fills do not need these state-view checks.

An ineligible view may still read cache hits; only its fill authority is
revoked. `WithFrontier` preserves the original epoch, so rebinding
cannot renew an old view. Stored entries remain O(1) to invalidate and
are discarded lazily. The three markers stay separate because durable
state, reader, and stored-entry lifetimes change at different
boundaries.

1. Staging an unwind revokes existing views, invalidates stored entries,
and records the lowest staged boundary. Bounded reads cannot fill.
2. Flush advances `PlainStateVersion` exactly once with the domain
writes and collects cache updates without publishing them.
3. The database transaction commits.
4. Cache publication applies the complete batch. It repeats unwind
invalidation at the durable boundary, rejects delayed or out-of-order
versions, preserves entries after a continuous forward commit, and
clears them when continuity is unknown.

During publication, reads and view binding remain available, but fills
are disabled. A view bound during publication remains fill-inert until
explicitly rebound.

`MemoryMutation` resolves untouched sequences from its backing
transaction and flushes only changed sequence keys, so it cannot replay
an older state version. Pre-commit notifications receive the projected
version explicitly rather than deriving it from overlay sequence writes.
Notification ordering itself is unchanged and remains tracked in #23240.

One semaphore permit covers read-ahead warmup and unwind exclusion. A
warmup acquires it without blocking, so work requested while another
warmup or an unwind owns or waits for the permit is skipped rather than
queued. Unwind callers acquire it with their context and abort before
staging if cancellation wins. `updateForkChoice` and `SetHead` hold the
permit through unwind and publication; `ValidateChain` acquires it only
when it stages an unwind. Every FCU currently excludes warmup, including
FCUs that do not unwind; narrowing that scope is tracked in #23003.

- The cache-hit path is unchanged.
- `View(nil)` adds one atomic load. Binding a fill-enabled `ReadView`
also takes `admissionMu.RLock` to check publication and state-version
eligibility; getters retain that view instead of paying the binding cost
per key.
- Normal getters reuse the `SharedDomains` transaction's memoized state
version. A different transaction resolves its version at initial
binding. If that resolution temporarily fails, later cache misses retry
it; each retry is local to that miss.
- Unwind invalidation remains O(1), with no cache scan or diff replay.
- Each accepted warmup performs one uncontended semaphore acquisition.
Rejected warmups do not start a goroutine, and the gate is never touched
per key.

Regression tests cover old and newly bound views across every unwind
phase, bounded state and code-hash reads, delayed publications,
memory-overlay state versions, read-ahead exclusion and cancellation,
and valid forward fills.

---------

Co-authored-by: Alexey Sharov <askalexsharov@gmail.com>
@yperbasis
yperbasis requested review from awskii and taratorio August 13, 2026 20:50
@yperbasis yperbasis added this to the 3.6.0 milestone Aug 13, 2026
@yperbasis
yperbasis enabled auto-merge August 13, 2026 20:50
@yperbasis
yperbasis added this pull request to the merge queue Aug 14, 2026
Merged via the queue into release/3.6 with commit 261ef9d Aug 14, 2026
94 checks passed
@yperbasis
yperbasis deleted the cherry-pick-23005-to-release-3.6 branch August 14, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants